using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.Diagnostics;

namespace Dida.Test
{
	public abstract class Runner
	{
		UnitTest test;

		protected Runner(UnitTest test)
		{
			this.test = test;
		}

		public abstract void Run(TestResultListenerWrapper resListener,
			RunnerStateInfo stateInfo, ref int nFailures, ProgressSegment progress);
		public abstract string Name { get; }

		public abstract int NumRuns { get; }

		protected void RunTest(IList<IParamAccessor> paramAccessors, IList<object> paramValues,
			TestResultListenerWrapper resListener, ref int nFailures, RunnerStateInfo runnerStateInfo)
		{
			Debug.Assert(paramAccessors.Count == paramValues.Count);

			try
			{
				object testObj = test.GetTestObject();

				IterationStateInfo stateInfo = new IterationStateInfo();
				stateInfo.CopyRunnerStateInfo(runnerStateInfo);
				stateInfo.iterationIndex = -1;

				stateInfo.paramValues = new ParamValue[paramAccessors.Count];
				for (int i = 0; i < paramAccessors.Count; i++)
				{
					paramAccessors[i].SetParam(testObj, paramValues[i]);

					stateInfo.paramValues[i].name = paramAccessors[i].ParamName;
					stateInfo.paramValues[i].value = paramValues[i];
				}

				resListener.BeginIteration(stateInfo);

				test.RunTestMethods(testObj, resListener, ref nFailures, stateInfo);

				resListener.EndIteration();
			}
			finally
			{
				test.ReleaseTestObject();
			}
		}
	}

	public class InstanceRunner : Runner
	{
		string name;
		UnitTest test;
		List<RunnerParam> parameters = new List<RunnerParam>();

		public InstanceRunner(UnitTest test, XmlReader xmlReader)
			: base(test)
		{
			this.test = test;

			name = xmlReader.GetAttribute("name");
			if (name == null)
			{
				name = "InstanceRunner";
			}

			if (xmlReader.IsEmptyElement)
			{
				Console.WriteLine("Warning: A \"Combinations\" element is empty");
				xmlReader.ReadStartElement();
				return;
			}

			xmlReader.ReadStartElement();
			while (xmlReader.NodeType != XmlNodeType.EndElement)
			{
				if (xmlReader.Name == "Param")
				{
					XmlPositionMarker runnerErrorPos = new XmlPositionMarker(xmlReader);

					RunnerParam param = new RunnerParam(test, xmlReader);
					if (!param.IsImmediateValue)
					{
						throw new XmlReaderException("All parameters of an \"Instance\" " +
							"runner must be immediate values.", runnerErrorPos);
					}

					parameters.Add(param);
				}
				else
				{
					xmlReader.Skip();
				}
			}
			xmlReader.ReadEndElement();
		}

		public override void Run(TestResultListenerWrapper resListener,
			RunnerStateInfo stateInfo, ref int nFailures, ProgressSegment progress)
		{
			IParamAccessor[] paramAccessors = new IParamAccessor[parameters.Count];
			object[] paramValues = new object[parameters.Count];
			for (int i = 0; i < parameters.Count; i++)
			{
				paramAccessors[i] = parameters[i].Accessor;
				paramValues[i] = parameters[i].ImmediateValue;
			}

			RunTest(paramAccessors, paramValues, resListener, ref nFailures, stateInfo);

			progress.Completed();
		}

		public override string Name
		{
			get { return name; }
		}

		public override int NumRuns
		{
			get { return 1; }
		}
	}

	public class CombinationsRunner : Runner
	{
		string name;
		UnitTest test;
		List<RunnerParam> parameters = new List<RunnerParam>();
		List<IParamAccessor> paramAccessors = new List<IParamAccessor>();

		public CombinationsRunner(UnitTest test, XmlReader xmlReader)
			: base(test)
		{
			this.test = test;

			name = xmlReader.GetAttribute("name");
			if (name == null)
			{
				name = "CombinationsRunner";
			}

			if (xmlReader.IsEmptyElement)
			{
				Console.WriteLine("Warning: A \"Combinations\" element is empty");
				xmlReader.ReadStartElement();
				return;
			}

			xmlReader.ReadStartElement();
			while (xmlReader.NodeType != XmlNodeType.EndElement)
			{
				if (xmlReader.Name == "Param")
				{
					RunnerParam param = new RunnerParam(test, xmlReader);
					parameters.Add(param);
					paramAccessors.Add(param.Accessor);
				}
				else
				{
					xmlReader.Skip();
				}
			}
			xmlReader.ReadEndElement();
		}

		public override void Run(TestResultListenerWrapper resListener,
			RunnerStateInfo stateInfo, ref int nFailures, ProgressSegment progress)
		{
			IEnumerator[] enumerators = new IEnumerator[parameters.Count];
			object[] paramValues = new object[parameters.Count];
			for (int i = 0; i < enumerators.Length; i++)
			{
				enumerators[i] = parameters[i].Values;
			}

			for (int i = 0; i < enumerators.Length; i++)
			{
				if (!enumerators[i].MoveNext())
				{
					Console.WriteLine("Warning: no combinations run, because one or more " +
						"parameter providers didn't provide any values");
					return;
				}
				paramValues[i] = enumerators[i].Current;
			}

			progress.NumSteps = NumRuns;

			RunTest(paramAccessors, paramValues, resListener, ref nFailures, stateInfo);
			progress.Step();

			while (true)
			{
				int cur = 0;
				while (!enumerators[cur].MoveNext())
				{
					// reset it (Reset method isn't supported by itererator 
					// functions (the ones using yield and stuff))
					enumerators[cur] = parameters[cur].Values;

					bool moveRes = enumerators[cur].MoveNext();
					Debug.Assert(moveRes);

					paramValues[cur] = enumerators[cur].Current;

					cur++;
					if (cur == enumerators.Length)
						return;
				}
				paramValues[cur] = enumerators[cur].Current;

				RunTest(paramAccessors, paramValues, resListener, ref nFailures, stateInfo);
				progress.Step();
			}
		}

		public override string Name
		{
			get { return name; }
		}

		public override int NumRuns
		{
			get
			{
				int ret = 1;
				foreach (RunnerParam param in parameters)
					ret *= param.NumParams;
				return ret;
			}
		}
	}

	public class SequenceRunner : Runner
	{
		string name;
		UnitTest test;
		List<RunnerParam> parameters = new List<RunnerParam>();
		List<IParamAccessor> paramAccessors = new List<IParamAccessor>();

		public SequenceRunner(UnitTest test, XmlReader xmlReader)
			: base(test)
		{
			this.test = test;

			name = xmlReader.GetAttribute("name");
			if (name == null)
			{
				name = "SequenceRunner";
			}

			if (xmlReader.IsEmptyElement)
			{
				Console.WriteLine("Warning: A \"SequenceRunner\" element is empty");
				xmlReader.ReadStartElement();
				return;
			}

			xmlReader.ReadStartElement();
			while (xmlReader.NodeType != XmlNodeType.EndElement)
			{
				if (xmlReader.Name == "Param")
				{
					RunnerParam param = new RunnerParam(test, xmlReader);
					parameters.Add(param);
					paramAccessors.Add(param.Accessor);
				}
				else
				{
					xmlReader.Skip();
				}
			}
			xmlReader.ReadEndElement();
		}

		public override void Run(TestResultListenerWrapper resListener,
			RunnerStateInfo stateInfo, ref int nFailures, ProgressSegment progress)
		{
			IEnumerator[] enumerators = new IEnumerator[parameters.Count];
			object[] paramValues = new object[parameters.Count];

			for (int i = 0; i < enumerators.Length; i++)
			{
				enumerators[i] = parameters[i].Values;

				bool moveNextRes = enumerators[i].MoveNext();
				Debug.Assert(moveNextRes, "The first MoveNext for a parameter should " +
					"never return false, since an empty parameter provider should have " +
					"been filtered out, and error-handled earlier on");

				paramValues[i] = enumerators[i].Current;
			}

			progress.NumSteps = NumRuns;

			bool done = false;
			while (!done)
			{
				RunTest(paramAccessors, paramValues, resListener, ref nFailures, stateInfo);
				progress.Step();

				// we are done when all enumerators have reached their end.
				// if that's not the case yet, the done flag will be set to 
				// false inside this loop.
				done = true;
				for (int i = 0; i < enumerators.Length; i++)
				{
					if (enumerators[i].MoveNext())
					{
						paramValues[i] = enumerators[i].Current;
						done = false;
					}
				}
			}
		}

		public override string Name
		{
			get { return name; }
		}

		public override int NumRuns
		{
			get
			{
				int ret = 0;
				foreach (RunnerParam param in parameters)
				{
					int curNumParams = param.NumParams;
					if (curNumParams > ret)
						ret = curNumParams;
				}
				return ret;
			}
		}
	}

	public class RunnerParam
	{
		IParamAccessor accessor;

		object immediateValue = null;
		List<IParamProvider> paramProviders = new List<IParamProvider>();

		public RunnerParam(UnitTest test, XmlReader xmlReader)
		{
			string paramName = xmlReader.GetAttribute("name");
			if (paramName == null)
				throw new XmlAttributeNotSpecifiedException(xmlReader, "name");
			accessor = test.GetParamAccessor(paramName);

			string value = xmlReader.GetAttribute("value");
			if (value != null)
			{
				try
				{
					immediateValue = ParsersRegister.Default.Parse(accessor.ParamType, value);
				}
				catch (ParsersRegister.ParserNotFoundException)
				{
					throw new XmlAttributeException(
						"This property/field doesn't support an immediate value", xmlReader, "value");
				}
				catch (Exception err)
				{
					throw new XmlAttributeException(err, xmlReader, "value");
				}

				// skip any child elements
				xmlReader.Skip();
			}
			else
			{
				if (xmlReader.IsEmptyElement)
					throw new XmlReaderException("A \"Param\" element should contain one or more Parameter Providers", xmlReader);
				xmlReader.ReadStartElement();

				Type paramType = accessor.ParamType;

				while (xmlReader.NodeType != XmlNodeType.EndElement)
				{
					switch (xmlReader.Name)
					{
					case "Range":
						if (paramType == typeof(int))
						{
							paramProviders.Add(new IntRangeParamProvider(xmlReader));
						}
						else if (paramType == typeof(float))
						{
							paramProviders.Add(new FloatRangeParamProvider(xmlReader));
						}
						else
						{
							throw new XmlReaderException("The type of this parameter " +
								"is incompatible with the \"Range\" provider (must be float or int)", xmlReader);
						}
						break;

					case "Value":
						paramProviders.Add(new ValueParamProvider(paramType, xmlReader));
						break;

					default:
						// skip any unknown nodes
						xmlReader.Skip();
						break;
					}
				}

				if (paramProviders.Count == 0)
				{
					throw new XmlReaderException("A \"Param\" element should contain one or more Parameter Providers", xmlReader);
				}

				xmlReader.ReadEndElement();
			}
		}

		public bool IsImmediateValue
		{
			get { return immediateValue != null; }
		}

		public IParamAccessor Accessor
		{
			get { return accessor; }
		}

		public IEnumerator Values
		{
			get
			{
				if (immediateValue != null)
				{
					yield return immediateValue;
				}
				else
				{
					foreach (IParamProvider paramProvider in paramProviders)
					{
						foreach (object cur in paramProvider.Values)
						{
							yield return cur;
						}
					}
				}
			}
		}

		/// <summary>
		/// Gets the immediate value that was specified for this parameter.
		/// </summary>
		/// <remarks>
		/// This property sould only be accessed when <see cref = "IsIntermediateValue"/>
		/// is true.
		/// </remarks>
		public object ImmediateValue
		{
			get
			{
				Debug.Assert(immediateValue != null, "This value isn't an immediate value");
				return immediateValue;
			}
		}

		public int NumParams
		{
			get
			{
				if (immediateValue != null)
					return 1;
				else
				{
					int ret = 0;
					foreach (IParamProvider paramProvider in paramProviders)
					{
						ret += paramProvider.NumParams;
					}
					return ret;
				}
			}
		}
	}	
}